import java.io.FileWriter;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.factory.GraphDatabaseFactory;
import org.neo4j.graphdb.index.Index;
import org.neo4j.graphdb.index.IndexHits;

/*
 * Менеджер графа
 * Ищет необработанные вершины, выдает их потокам, добавляет вершины и связи
 * 
 * Индекс хранит id вершины и её состояние. при изменении состоянии переписывается только состояние в индексе
 */

public class GraphManager {
	
	private static final String NODES_INDEX = "user_nodes";

	private static final String PROPERTY_USER_ID = "user_id";	//Long	
	private static final String PROPERTY_USER_NAME = "user_name";//Нельзя хранить ничего, кроме стандартных типов.Будем хранить имя
	private static final String PROPERTY_STATE = "state";		//Integer
	//Значения PROPERTY_STATE
	private static final int STATE_IN_PROCESSING = 1;
	private static final int STATE_WAIT_PROCESSING = 2;
	
	public static final int STATE_JUST_ADDED = 3;
	public static final int STATE_FOLLOWERS_RECEIVED = 4;
	public static final int STATE_GETTING_FOLLOWERS_ENDED = 5;
	public static final int STATE_USERDATA_RECEIVED = 6;
	public static final int STATE_USERMESSAGES_RECEIVED = 7;
	
	//сервис
	private GraphDatabaseService graphDb;
	private Index<Node> index;
	
	public GraphManager(Long headuserid) {
		//создаем бд
		graphDb = new GraphDatabaseFactory().newEmbeddedDatabase("./db/");
		index = graphDb.index().forNodes(NODES_INDEX);
		
		//создаем основного юзера, от которого будут идти связи
		Transaction tx = graphDb.beginTx();
		try {
			getUserNode(headuserid);
		    tx.success();
		}
		finally {
		    tx.finish();
		}
	}
	
	//Если ноды нет - добавляет, если есть - возвращает
	//need transaction!
	private Node getUserNode(Long userid) {
		//ищем ноду с userId
		Node user = index.get(PROPERTY_USER_ID, userid).getSingle();
		
		if (user == null) {
			//если нет - создаем ноду, устанавливаем свойства
			user = graphDb.createNode();
			Integer state = new Integer(STATE_JUST_ADDED);
			user.setProperty(PROPERTY_USER_ID, userid);
			//на будущее
			//индекс
			index.add(user, PROPERTY_USER_ID, userid);
			index.add(user, PROPERTY_STATE, state);
		}
		
		return user;
	}
	
	//Добавить фолловеров юзера
	public synchronized void addUserFollowers(Long userId, List<Long> followersIds) {
		Transaction tx = graphDb.beginTx();
		try {
			//получаем ноду
			Node user = getUserNode(userId);
			//создаем связи для всех друзей
			for (Long followerId : followersIds) {
				//ищем юзера
				Node follower = getUserNode(followerId);
				//связь
				follower.createRelationshipTo(user, RelationTypes.FOLLOWS);
			}
		    tx.success();
		}
		finally {
		    tx.finish();
		}
	}
	//обрабатываем только just_added!
	public void startFollowersProcessing() {
		Integer oldValue = new Integer(STATE_JUST_ADDED);
		IndexHits<Node> hits = index.get(PROPERTY_STATE, oldValue);
		if (hits != null)
		 try {
	    	 Transaction tx = graphDb.beginTx();
	    	 try {	
				 //перебираем все
			     for (Node node : hits) {
			    	 //заменяем стейт на "ожидание обработки"
			    	 index.remove(node, PROPERTY_STATE);
			    	 index.add(node, PROPERTY_STATE, new Integer(STATE_WAIT_PROCESSING));
			     }
			     tx.success();
	    	 } finally {
	    	     tx.finish();
	    	 }
		     System.out.println("GraphManager.startFollowersProcessing():Start to followers processing, count of users:" + hits.size());
		 }
		 finally {
		     hits.close();
		 }
	}
	
	public void endUserFollowersProccessing(Long userid) {
		Node user = index.get(PROPERTY_USER_ID, userid).getSingle();
		if (user != null) {
			Transaction tx = graphDb.beginTx();
			try {	
				//заменяем стейт на "фолловеры получены"
				index.remove(user, PROPERTY_STATE);
				index.add(user, PROPERTY_STATE, new Integer(STATE_FOLLOWERS_RECEIVED));
				tx.success();
			} finally {
				tx.finish();
			}
		} 
		else {
			System.err.println("GraphManager.endUserFriendsProccessing():unrecognized user with id " + userid.toString());
		}
	}

	//Начинаем обработку данных по юзерам(после получения всех follower'ов)
	public void startUsersDataProcessing() {
		int count = 0;
		IndexHits<Node> hits = index.query(PROPERTY_STATE, "*");
		
		count += hits.size();
		
		try {
			Transaction tx = graphDb.beginTx();
			try {	
				//перебираем все
				for (Node node : hits) {
					//заменяем стейт на "ожидание обработки"
					index.remove(node, PROPERTY_STATE);
					index.add(node, PROPERTY_STATE, new Integer(STATE_WAIT_PROCESSING));
				}
				tx.success();
			} 
			finally {
				tx.finish();
			}			
		} 
		finally {
			hits.close();
		}
		
		System.out.println("GraphManager.startUsersDataProcessing():Start to user data processing, count of users:" + count);
	}
	
	//завершаем обработку данных для юзера
	public void endUserDataProccessing(Long userid, Responses.UserDataResponse response) {
		Node user = index.get(PROPERTY_USER_ID, userid).getSingle();
		if (user != null) {
			Transaction tx = graphDb.beginTx();
			try {	
				//добавляем данные из поля
				if (response.screen_name != null)
					user.setProperty(PROPERTY_USER_NAME, response.screen_name);
				else {
					//если данных нет - пропишем id
					user.setProperty(PROPERTY_USER_NAME, userid.toString());
				}
				//заменяем стейт на "данные получены"
				index.remove(user, PROPERTY_STATE);
				index.add(user, PROPERTY_STATE, new Integer(STATE_USERDATA_RECEIVED));
				tx.success();
			} finally {
				tx.finish();
			}
		} 
		else {
			System.err.println("GraphManager.endUserDataProccessing():unrecognized user with id " + userid.toString());
		}
	}

	
	//Получить Id юзера, для которого нужно провести обработку или null, если таких нет
	public synchronized Long getNextUserIdForProcessing() {
		Integer oldState = new Integer(STATE_WAIT_PROCESSING);
		IndexHits<Node> users = index.get(PROPERTY_STATE, oldState);
		Node user = null;
		
		try {
			//дурацкий способ получения 1ой записи
			if (users.hasNext())
				user = users.next();
		}
		finally {
			users.close();
		}
		
		if (user != null) {
			Transaction tx = graphDb.beginTx();
			try {	
				//заменяем стейт на "в обработке"
				index.remove(user, PROPERTY_STATE, oldState);
				index.add(user, PROPERTY_STATE, new Integer(STATE_IN_PROCESSING));
				tx.success();
			} finally {
				tx.finish();
			}
			return (Long)user.getProperty(PROPERTY_USER_ID);
		}
		
		return null;
	}
	
	//Проверить, у всех ли юзеров закончилась обработка
	public boolean checkAllProccessingsEnded() {
		IndexHits<Node> processingHits = index.get(PROPERTY_STATE, new Integer(STATE_IN_PROCESSING));
		IndexHits<Node> waitprocessingHits = index.get(PROPERTY_STATE, new Integer(STATE_WAIT_PROCESSING));
		//если все обработаны - в процессе никого не останется
		return (processingHits.size() + waitprocessingHits.size()) == 0;
	}
	
	//Получить количество народу в нодах
	public long getTotalUsersCount() {
		IndexHits<Node> allNodes = index.query(PROPERTY_STATE, "*");		
		return allNodes.size();
	}
	
	//Получить количество связей в нодах
	public long getTotalUsersRelationshipsCount() {
		IndexHits<Node> allNodes = index.query(PROPERTY_STATE, "*");	
		long count = 0;
		//headnodes
		if (allNodes != null) {
			try {
				for (Node n : allNodes) {
					//проходим по всем связям
					Iterable<Relationship> relationShips =  n.getRelationships();
					Iterator<Relationship> relationIt = relationShips.iterator();
					while (relationIt.hasNext()) {
						Relationship relation = relationIt.next();
						//считаем связи только в одну сторону
						if (relation.getEndNode().getProperty(PROPERTY_USER_ID).equals(n.getProperty(PROPERTY_USER_ID)))
							count++;
					}
				}
			} 
			finally {
				allNodes.close();
			}

		}
		
		return count;
	}
	
	//2 параметр - формат выдачи, нужно ли выводить в графвиз. если нет - пишем в обычный csv
	public void writeFollowersToFile(String fileName, boolean exportToGraphWiz) {
		try
		{
		    FileWriter writer = new FileWriter(fileName);
	 
		    if (exportToGraphWiz) {
		    	//overlap=false для генерации картинки в neoto
		    	writer.append("digraph G {graph [overlap=false];");
		    } else {
		    	//экспорт в csv - запишем верхнюю колонку с названиями полей
			    writer.append("Follower");
			    writer.append(',');
			    writer.append("Followee");
			    writer.append('\n');
		    }
		    
			IndexHits<Node> allNodes = index.query(PROPERTY_STATE, "*");	
			//headnodes
			if (allNodes != null) {
				try {
					for (Node n : allNodes) {
						//Получим id вершины
						Long userId = (Long)n.getProperty(PROPERTY_USER_ID);
						String userName = (String)n.getProperty(PROPERTY_USER_NAME);
						
						//проходим по всем связям
						Iterable<Relationship> relationShips =  n.getRelationships();
						Iterator<Relationship> relationIt = relationShips.iterator();
						while (relationIt.hasNext()) {
							Relationship relation = relationIt.next();
							
							Node follower = relation.getStartNode();
							Long followerId = (Long)follower.getProperty(PROPERTY_USER_ID);
							String followerName = (String)follower.getProperty(PROPERTY_USER_NAME);
							
							//проверим, что связь идет к текущему юзеру
							if (relation.getEndNode().getProperty(PROPERTY_USER_ID).equals(userId)) {
								
								if (exportToGraphWiz) {
									writer.append('\"');
								    writer.append(followerName != null ? followerName : followerId.toString());
									writer.append('\"');
								    writer.append("->");
									writer.append('\"');
								    writer.append(userName != null ? userName : userId.toString());
									writer.append('\"');
								    writer.append(';');							
								} else {
									//csv
								    writer.append(followerId.toString());
								    writer.append(',');
								    writer.append(userId.toString());
								    writer.append('\n');
								}
							}
						}
					}
				} 
				finally {
					allNodes.close();
				}

			}
			
			if (exportToGraphWiz) {
				writer.append("}");
			}
			
			//закрываем файл
		    writer.flush();
		    writer.close();
		}
		catch(IOException e)
		{
		     e.printStackTrace();
		} 
	}
}
